package ua.edu.nuos.jeetraining2012.cms.model;

import org.slf4j.Logger;
import ua.edu.nuos.jeetraining2012.cms.entity.CourseMaterialEntity;
import ua.edu.nuos.jeetraining2012.cms.model.exception.DAOException;

import javax.ejb.Local;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * MaterialDAO.
 * <p/>
 * DAO layer for course materials.
 *
 * @author Victor N. Ganichev victor.ganichev@gmail.com
 * @version 1.0 Date: 02.06.13 Time: 19:25
 * @since 1.0
 */
@Stateless
@Local
public class MaterialDAO {

    /**
     * Define EntityManager.
     */
    @PersistenceContext
    private EntityManager em;

    /**
     * Define logger.
     */
    final private static Logger logger = org.slf4j.LoggerFactory.getLogger(MaterialDAO.class);

    // Constructors.

    /**
     * Default constructor.
     */
    public MaterialDAO() {
        logger.debug("MaterialDAO invocation");
    }

    /**
     * Controller with predefined EntityManager.
     *
     * @param em        Instance of EntityManager.
     */
    public MaterialDAO(EntityManager em) {
        logger.debug("MaterialDAO invocation with predefined EntityManager {}", em);
        setEm(em);
    }

    // Getters & Setters.

    protected EntityManager getEm() {
        return em;
    }

    private void setEm(EntityManager em) {
        this.em = em;
    }

    // Custom methods.

    /**
     * Find all course materials by course identifier.
     * This method return list of {@link CourseMaterialEntity} in case of success or
     * empty list if nothing was found.
     * <p/>
     * If courseId sent to method is null {@link IllegalArgumentException} will thrown.<br/>
     * If sth goes wrong, when querying DB, {@link DAOException} will thrown.
     *
     * @param courseId                      Long course ID.
     * @return                              List of CourseMaterialEntity objects or empty List..
     * @throws IllegalArgumentException     Thrown if course id is null or course id is less then 1.
     * @throws DAOException                 Thrown if any parent exception occurred in querying.
     */
    @SuppressWarnings("unchecked")
    public List<CourseMaterialEntity> findAllMaterialsByCourse(Long courseId)
            throws IllegalArgumentException, DAOException {
        logger.debug("findAllMaterialsByCourse() - started.");

        if (courseId == null || courseId < 1) {
            logger.error("Course ID can't be {}.", courseId);
            logger.debug("findAllMaterialsByCourse() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("Course material can't be null or less then 1");
        }
        List<CourseMaterialEntity> materials = null;

        try {
            String sql = "SELECT m.* FROM materials m WHERE m.course_id = ?";
            Query q = em.createNativeQuery(sql, CourseMaterialEntity.class);
            logger.debug("Fetching materials with query: {}", q);

            materials = q.setParameter(1, courseId).getResultList();
            logger.debug("Found {} materials.", materials.size());
        } catch (Exception e) {
            logger.error("Can't find material. Message: {}, \nTrace:\n {}", e.getMessage(),
                    Arrays.deepToString(e.getStackTrace()));
            logger.debug("findAllMaterialsByCourse() - finished with DAOException.");
            throw new DAOException("Can't find material. Error message: " + e.getMessage());
        }

        logger.debug("findAllMaterialsByCourse() - finished.");
        return materials;
    }

    /**
     * Find material by it's ID.
     * <p/>
     * This method return Instance of {@link CourseMaterialEntity} by its ID,
     * if any exists in DB. If record can't be fount, null will be returned.
     * <p/>
     * In case, when materialId is null or less then 1, IllegalArgumentException will
     * thrown.
     * In case, when EntityManager.find() throw Exception, DAOException will thrown.
     *
     * @param materialId                    Long material ID.
     * @return                              Instance of CourseMaterialEntity or null.
     * @throws IllegalArgumentException     When illegal materialId sent.
     * @throws DAOException                 When parent Exception presents.
     */
    public CourseMaterialEntity findMaterialById(Long materialId) throws IllegalArgumentException, DAOException {
        logger.debug("findMaterialById() - started");

        if (materialId == null || materialId < 1) {
            logger.error("findMaterialById() materialId can't be {}", materialId);
            logger.debug("findMaterialById() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("material id can't be null or less then 1.");
        }

        CourseMaterialEntity material = null;

        try {
            logger.debug("findMaterialById() Trying to find Materials by id: {}", materialId);

            material = em.find(CourseMaterialEntity.class, materialId);

            logger.debug("findMaterialById() Result: {}", material.toString());
        } catch (Exception e) {
            logger.error("findMaterialById() Can't find Material Error message: {}, \nTrace:\n {}", e.getMessage(),
                    Arrays.deepToString(e.getStackTrace()));
            logger.debug("findByMaterialId() - finished with DAOException.");

            throw new DAOException("Can't find material. Message: " + e.getMessage());
        }
        logger.debug("findMaterialById() Returned material: {}", material);

        logger.debug("findMaterialById() - finished.");
        return material;
    }

    /**
     * Find Section & Material titles according to sent course ID.
     * In case, when query succeed, List of all possible titles will be returned.
     * <table border=1>
     *     <thead>
     *         <tr>
     *             <th>Section title</th>
     *             <th>Title</th>
     *         </tr>
     *     </thead>
     *     <tbody>
     *         <tr>
     *             <td>Week1</td>
     *             <td>Introduction</td>
     *         </tr>
     *         <tr>
     *             <td>Week1</td>
     *             <td>....</td>
     *         </tr>
     *     </tbody>
     *
     * </table>
     * If no data can't be achieved, <strong>null</strong> will be returned.
     *
     * @param courseId                  Long course ID.
     * @return                          List of MaterialTitlesDTO or null.
     * @throws IllegalArgumentException If sent courseId null or less then 1.
     * @throws DAOException             In case of parent exception during command execution.
     */
    @SuppressWarnings("unchecked")
    public List<CourseMaterialEntity> findTitlesByCourseId(Long courseId) throws IllegalArgumentException, DAOException {
        logger.debug("findTitlesByCourseId() - started.");

        if (courseId == null || courseId < 1) {
            logger.error("courseId can't be {}", courseId);
            logger.debug("findTitlesByCourseId() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("Titles can't be retrieved. ID can't be null or less then 1.");
        }

        List<CourseMaterialEntity> materials = new ArrayList<>();

        final String sql = "SELECT m.id, m.section_title AS section_title, m1.title AS title FROM materials m " +
                "INNER JOIN materials m1 ON (m1.id = m.id) " +
                "WHERE m.course_id = ?1";

        try {
            Query q = em.createNativeQuery(sql, "MaterialsTitlesReMapping").setParameter(1, courseId);
            logger.debug("Trying to find titles with query: {}", q);

            List<Object[]> resultList = q.getResultList();
            logger.debug("Found {} titles", resultList.size());

            if (resultList != null && ! resultList.isEmpty()) {
                for (Object[] resArray : resultList) {
                    CourseMaterialEntity dto = new CourseMaterialEntity();
                    dto.setId(Long.valueOf(String.valueOf(resArray[0])));
                    dto.setSectionTitle(String.valueOf(resArray[1]));
                    dto.setTitle(String.valueOf(resArray[2]));
                    materials.add(dto);
                }
            } else {
                materials = null;
            }
            logger.debug("Converted materials: {}", materials);

        } catch (Exception e) {
            logger.error("Can't find any title. Error message: {}, \nTrace:\n {}", e.getMessage(),
                    e.getStackTrace());
            logger.debug("findTitlesByCourseId() - finished with DAOException..");
            throw new DAOException("Titles can't be retrieved. Error Message: " + e.getMessage());
        }

        logger.debug("findTitlesByCourseId() - finished.");
        return materials;
    }

    /**
     * Find section titles for course.
     * <p/>
     * Return list of available section titles.
     *
     * @param courseId                      Course ID.
     * @return                              List of available section titles.
     * @throws IllegalArgumentException     If course ID null or less than 1.
     * @throws DAOException                 If parent exception appeared.
     */
    public List<String> findSectionTitles(Long courseId) throws IllegalArgumentException, DAOException {
        logger.debug("findSectionTitles() - started.");

        if (courseId == null || courseId < 1) {
            logger.error("CourseId cant be {}", courseId);
            logger.debug("findSectionTitles - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("Cousrse ID can't be null or less that 1.");
        }

        List<String> materialsTitles = new ArrayList<>();

        final String sql = "SELECT distinct(m.section_title) FROM materials m WHERE m.course_id = ?1";

        logger.debug("Trying to find section titles to course with ID {}", courseId);
        try {
            List<Object[]> resultList = null;
            Query q = em.createNativeQuery(sql).setParameter(1, courseId);
            resultList = q.getResultList();
            logger.info("result is: {}", resultList);

            if (resultList != null && !resultList.isEmpty()) {
                for (Object resArray : resultList) {
                    materialsTitles.add(String.valueOf(resArray));
                }
            }
            logger.debug("Converted section titles list {}", materialsTitles);
        } catch (Exception e) {
            logger.error("Can't find section titles, got Exception with message {} and \nTrace:\n {}", e.getMessage(),
                    Arrays.deepToString(e.getStackTrace()));
            logger.debug("findSectionTitles() - finished with DAOException");
            throw new DAOException("Section titles can't be found.");
        }

        logger.debug("findSectionTitles() - finished.");
        return materialsTitles;
    }


    /**
     * Create new material record.
     * <p/>
     * You should send filled in CourseMaterialEntity object. In case of success, this method MUST return
     * id of inserted object.
     *
     * @param dto Instance of CourseMaterialEntity.
     * @return Id of inserted bean.
     * @throws DAOException             in cases, when can't create material record.
     * @throws IllegalArgumentException in case when CourseMaterialEntity object is null.
     */
    public long create(CourseMaterialEntity dto) throws DAOException, IllegalArgumentException {
        logger.debug("create() - start.");

        if (dto == null) {
            logger.warn("NULL UserEntity");
            logger.debug("create() - finish with NPE.");
            throw new IllegalArgumentException("Can't persists null valued entity");
        }

        long materialId;
        try {
            em.persist(dto);
            em.flush();
            materialId = dto.getId();
        } catch (Exception e) {
            logger.warn("Error! Can't add material.\nMessage: " + e.getMessage() + "\nTrace:\n" +
                    Arrays.deepToString(e.getStackTrace()));
            logger.debug("create() - finish with DAOException.");
            throw new DAOException("Unable to add material. Error message: " + e.getMessage());
        }

        logger.debug("create() - finish.");
        return materialId;
    }

    /**
     * Delete material.
     * <p/>
     * Delete existent material from DB. You have to send DTO with filled in material id.
     * In case of success, true will be returned.
     *
     * @param dto Instance of CourseMaterialEntity object with filled in id.
     * @return true in case of success. Either -- false.
     * @throws DAOException             in case, when can't delete user or parent exceptions appeared.
     * @throws IllegalArgumentException in case, when Entity is null.
     */
    public boolean delete(CourseMaterialEntity dto) throws DAOException, IllegalArgumentException {
        logger.debug("delete() - start.");

        if (dto == null) {
            logger.warn("NULL UserEntity");
            logger.debug("delete() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("Can't delete with null valued Entity.");
        }

        boolean isDeleted = false;
        try {
            CourseMaterialEntity material = em.find(CourseMaterialEntity.class, dto.getId());
            em.remove(material);
            isDeleted = true;
            logger.debug("Material success deleted.");
        } catch (Exception e) {
            logger.warn("Error! Can't delete user. Message: " + e.getMessage() + "\nTrace:\n" +
                    Arrays.deepToString(e.getStackTrace()));
            logger.debug("delete() - finished with DAOException.");
            throw new DAOException("Unable to delete user. Error message: " + e.getMessage());
        }

        logger.debug("delete() - finish.");
        return isDeleted;
    }


    /**
     * Update material.
     * <p/>
     * Update material with send CourseMaterialEntity. To update material, please set in this bean its id
     * and changed fields. After that just send those DTO to this method.
     * In case of success, true will be returned.
     *
     * @param dto Instance of changed CourseMaterialEntity
     * @return True in case of success, either return false.
     * @throws DAOException             in case, when can't update material or parent exception appeared.
     * @throws IllegalArgumentException in case, when CourseMaterialEntity is null.
     */
    public boolean update(CourseMaterialEntity dto) throws DAOException, IllegalArgumentException {
        logger.debug("update() - start.");
        boolean isUpdated = false;

        if (dto == null) {
            logger.warn("NULL UserEntity");
            logger.debug("update() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("Can't update with null valued Entity.");
        }

        try {
            em.merge(dto);
            isUpdated = true;
            logger.debug("Material updated success.");
        } catch (Exception e) {
            logger.warn("Error! Can't material user. Message: " + e.getMessage() + "\nTrace:\n" +
                    Arrays.deepToString(e.getStackTrace()));
            logger.debug("update() - finished with DAOException.");
            throw new DAOException("Unable to update material. Error message: " + e.getMessage());
        }
        logger.debug("update() - finish.");
        return isUpdated;
    }

}
